Cocos Creator 的 3D 寻路实现探索:NavMesh 导航网格
「寻路」是很多游戏不可或缺的功能。2D 游戏中最常用的就是 A* 寻路了。而在 3D 游戏中,对于一些简单的、没有高度的地面,A* 寻路也是可以使用的;但是对于一些地面比较复杂的游戏,寻路功能怎样实现比较好呢?
本文将分析 RecastNavigation、耳切法 +A*、NavMesh 导航这三种方案,找找效果最佳、最适合 Cocos Creator 的寻路实现方案。
RecastNavigation
相信了解过 3D 寻路的小伙伴都有听说过 RecastNavigation。RecastNavigation 是一款非常强大的寻路系统,被广泛的应用于各大主流游戏引擎中。
NavMesh 的生成基本原理:
体素化。从源几何体构造实心的高度场,用来表示不可行走的空间。
生成地区。将实心高度场的上表面中连续的区间合并为地区。
生成轮廓。检测地区的轮廓,并构造成简单多边形。
生成多边形网格。将轮廓分割成凸多边形。
生成高度细节。将多边形网格三角化,得到高度细节。
感兴趣的小伙伴们可以去了解一下,甚至可以去了解了解源码。
耳切法 +A*
了解了导航寻路的原理后,我认为寻路的关键分为两部分:网格画与寻路。
我们先从最简单的没有高度的地图入手,大概步骤如下:
基于耳切法实现地图的网格化;
基于 A* 实现寻路;
基于漏斗算法将三角形集合进行路径最优解。
经过尝试,基本的寻路功能是实现了,但是在路径的优化方面会存在一些问题,所以我决定去寻找一些现成的 js 库来实现这个功能。
NavMesh 导航
在 github 闲逛的时候发现了 recast.js 这个库,于是了解了一下它的具体功能和 API。花了点时间,成功将 recast.js 移到了 Cocos Creator 3.x。
接着尝试用 recast.js 实现寻路。
初始化
import Recast from "./lib/recast.js"
...
public init(cb: Function = null):void{
new Recast().then((recast) => {
this._recast = recast;
this._navMesh = new this._recast.NavMesh();
this.setDefaultConfig();
this._tempVec=new this._recast.Vec3();
this._tempVec1=new this._recast.Vec3();
this._tempVec2=new this._recast.Vec3();
if (cb) cb();
});
}
添加静态物体
/**
* 添加静态的模型
*/
public addStaticModle(node: Node): void {
if(!node||!node.getComponent(MeshRenderer)) return;
let render: MeshRenderer = node.getComponent(MeshRenderer);
if (!render || !render.mesh) return;
let matrix: math.Mat4 = node.getWorldMatrix();
this.updateBaseDatas(render.mesh, matrix);
}
场景中一些固定不变的,比如地面、障碍物之类的可以通过这个结构进行添加,代码中对传入节点的网格信息进行处理,处理为 recast 所需要的格式。
添加 Terrain 地形
/**
* 添加cocos的地形
* @param terrain 地形组件
*/
public addTerrain (terrain: Terrain,): any {
...
}
terrain 地形组件同时也是游戏开发过程中会常用到的组件。听取了大佬的建议,将咱们的寻路对 terrain 进行了支持,大家可以在 Demo 中看见对 terrain 数据处理的一些逻辑。
构建导航网格
/**
* 构建导航网格
*/
public build(): void {
var rc = new this._recast.rcConfig();
rc.cs = this._config.cs;
rc.ch = this._config.ch;
....
this._navMesh.build(this._positions, this._positions.length / 3, this._indices, this._indices.length, rc);
}
可视化调试
在构建完导航网格后,为了方便调试,将最终的数据以可视化的效果进行展现。代码中已经做好了处理,提供了两种调试方式:
export enum MeshDebugDataType {
SURFACE = 0, //面
LINE = 1, //线
}
let navMeshData: NavMeshDebugData = this._navMeshMgr.getNavMeshDebugData(MeshDebugDataType.LINE);
this.createDebugMesh(navMeshData.positions, navMeshData.normals);
private createDebugMesh (positions: number[], normals: number[]): void {
this.line.node.destroyAllChildren();
let node = new Node();
let render = node.addComponent(MeshRenderer);
let mesh = utils.createMesh({
positions: positions,
primitiveMode: gfx.PrimitiveMode.TRIANGLE_LIST,
normals: normals,
});
render.mesh = mesh;
this.line.node.addChild(node);
}
Crowd 和 Agent
首先谈谈本人对这两个概念的理解吧:
Crowd,从字面意识大概就能猜到,即人群。人群可以有多个。
Agent,表示的是这群人里边的其中一个。
我推测每个 Crowd 中的 Agent 在移动的时候会进行彼此之间距离的一些检测,但是不同 Crowd 的 Agent 则不会检测(还未验证)。
首先创建 Crowd:
public initCrowd (maxAgents: number, maxAgentRadius: number): NavMeshCorwd {
return new NavMeshCorwd(this, maxAgents, maxAgentRadius);
}
添加一个 Agent:
public addAgent (pos: Vec3, parameters: AgentConfig): number {
let config = new this._navMeshMgr.recast.dtCrowdAgentParams();
...
let agentIndex: number = this._recastCrowd.addAgent(new this._navMeshMgr.recast.Vec3(pos.x, pos.y, pos.z), config);
...
return agentIndex;
}
寻路移动
导航网格的关键点就是为了获取两点之间的最有路径。
private findPath (pos): void {
let targetPos: Vec3 = this._navMeshMgr.getClosestPoint(pos);
//设置指定的Agent移动到特定位置
this._navMeshCrowd.agentMoveTarget(this._playerAgentIndex, targetPos);
}
通过 agentMoveTarget() 函数去置顶 Agent 的目标点,同时还需要去实时刷新 Crowd 来更新 Agnet 的实际位置。
update (deltaTime: number) {
if(! this._navMeshCrowd) return;
//刷新Crowd
this._navMeshCrowd.update(deltaTime);
//获取指定Agent当前的位置
let agentPosition = this._navMeshCrowd.getAgentPosition(this._playerAgentIndex);
this.player.position = agentPosition;
}
希望以上内容能对小伙伴们有所帮助。点击文末【阅读原文】前往 Cocos Store 下载 Demo。
本文作者为搬砖小菜鸟
欢迎关注他的个人公众号与他交流↓